﻿#region Usings

using System;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;

#endregion

namespace Yandex.Market.Api.YmlSerialization
{
    /// <summary>
    ///   Provides utility methods for manipulating XML.
    ///   There are four methods for each unit. UnitExists, FindUnit, CanCreateUnit, CreateUnit
    ///   Units are: Location, Element, and Attribute
    /// </summary>
    internal static class XmlUtils
    {
        /// <summary>
        ///   Determines whether the location specified exists in the given XML element.
        /// </summary>
        /// <param name = "baseElement">The XML element.</param>
        /// <param name = "location">The location string.</param>
        /// <returns>a value indicating whether the location specified exists in the given XML element</returns>
        public static bool LocationExists(XElement baseElement, string location)
        {
            XElement newLoc = FindLocation(baseElement, location);
            return newLoc != null;
        }

        /// <summary>
        ///   Finds the location specified in the given XML element specified.
        /// </summary>
        /// <param name = "baseElement">The XML element.</param>
        /// <param name = "location">The location string.</param>
        /// <returns>the XML element corresponding to the specified location, or <c>null</c> if it is not found</returns>
        public static XElement FindLocation(XElement baseElement, string location)
        {
            if(baseElement == null || location == null)
                throw new ArgumentNullException();

            string[] locSteps = location.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries);

            XElement currentLocation = baseElement;
            foreach(string loc in locSteps)
            {
                if(loc == ".")
                    continue;
                if(loc == "..")
                {
                    currentLocation = currentLocation.Parent;
                    if(currentLocation == null)
                        break;
                }
                else
                {
                    currentLocation = currentLocation.Element(loc);
                    if(currentLocation == null)
                        break;
                }
            }

            return currentLocation;
        }

        /// <summary>
        ///   Determines whether the specified location can be created in the specified XML element.
        /// </summary>
        /// <param name = "baseElement">The XML element.</param>
        /// <param name = "location">The location string.</param>
        /// <returns>
        ///   <c>true</c> if the specified location can be created in the specified XML element; otherwise, <c>false</c>.
        /// </returns>
        public static bool CanCreateLocation(XElement baseElement, string location)
        {
            string[] locSteps = location.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries);

            XElement currentLocation = baseElement;
            foreach(string loc in locSteps)
            {
                if(loc == ".")
                    continue;
                if(loc == "..")
                {
                    currentLocation = currentLocation.Parent;
                    if(currentLocation == null)
                        return false;
                }
                else
                {
                    currentLocation = currentLocation.Element(loc);
                    if(currentLocation == null)
                        return true;
                }
            }

            return true;
        }

        /// <summary>
        ///   Creates and returns XML element corresponding to the sepcified location in the given XML element.
        /// </summary>
        /// <param name = "baseElement">The XML element.</param>
        /// <param name = "location">The location string.</param>
        /// <returns>XML element corresponding to the sepcified location created in the given XML element</returns>
        public static XElement CreateLocation(XElement baseElement, string location)
        {
            string[] locSteps = location.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries);

            XElement currentLocation = baseElement;
            foreach(string loc in locSteps)
            {
                if(loc == ".")
                    continue;
                if(loc == "..")
                {
                    currentLocation = currentLocation.Parent;
                    if(currentLocation == null)
                        break;
                }
                else
                {
                    XElement newLoc = currentLocation.Element(loc);
                    if(newLoc == null)
                    {
                        newLoc = new XElement(loc);
                        currentLocation.Add(newLoc);
                        currentLocation = newLoc;
                    }
                }
            }

            return currentLocation;
        }

        /// <summary>
        ///   Determines whether the attribute with the given name located in the given location string exists in the given XML element.
        /// </summary>
        /// <param name = "baseElement">The XML element.</param>
        /// <param name = "location">The location string.</param>
        /// <param name = "attrName">Name of the attribute.</param>
        /// <returns>
        ///   a value indicating whether the attribute with the given name located in the given location string exists in the given XML element.
        /// </returns>
        public static bool AttributeExists(XElement baseElement, string location, string attrName)
        {
            return FindAttribute(baseElement, location, attrName) != null;
        }

        /// <summary>
        ///   Finds the attribute with the given name located in the given location string in the given XML element.
        /// </summary>
        /// <param name = "baseElement">The XML element.</param>
        /// <param name = "location">The location string.</param>
        /// <param name = "attrName">Name of the attribute.</param>
        /// <returns>a value indicating whether the attribute with the given name located in 
        ///   the given location string in the given XML element has been found.</returns>
        public static XAttribute FindAttribute(XElement baseElement, string location, string attrName)
        {
            XElement newLoc = FindLocation(baseElement, location);
            if(newLoc == null)
                return null;

            return newLoc.Attribute(attrName);
        }

        /// <summary>
        ///   Determines whether the attribute with the given name can be created in the location 
        ///   specified by the given location string in the given XML element.
        /// </summary>
        /// <param name = "baseElement">The XML element.</param>
        /// <param name = "location">The location string.</param>
        /// <param name = "attrName">Name of the attribute.</param>
        /// <returns>
        ///   <c>true</c> if the attribute with the given name can be created in the location 
        ///   specified by the given location string in the given XML element; otherwise, <c>false</c>.
        /// </returns>
        public static bool CanCreateAttribute(XElement baseElement, string location, string attrName)
        {
            XElement newLoc = FindLocation(baseElement, location);
            if(newLoc == null) //if the location does not exist
            {
                return CanCreateLocation(baseElement, location);
                // if you can't create the location you can't create the attribute either
            }

            return newLoc.Attribute(attrName) == null; // i.e., check if the attribute does not exist
        }

        /// <summary>
        ///   Creates and returns the attribute with the given name in the location 
        ///   specified by the given location string in the given XML element.
        /// </summary>
        /// <param name = "baseElement">The XML element.</param>
        /// <param name = "member"></param>
        /// <param name = "attrValue">The value to be assigned to the attribute.</param>
        /// <returns>returns the attribute with the given name in the location 
        ///   specified by the given location string in the given XML element.</returns>
        public static XAttributeWrapper CreateAttribute(XElement baseElement, MemberWrapper member, object attrValue)
        {
            XElement newLoc = FindLocation(baseElement, member.SerializationLocation);
            if(newLoc == null)
            {
                if(CanCreateLocation(baseElement, member.SerializationLocation))
                    newLoc = CreateLocation(baseElement, member.SerializationLocation);
                else
                    return null;
            }

            if(newLoc.Attribute(member.Alias) != null) // i.e., the attribute already exists 
                return null; // we cannot create another one with the same name

            XAttribute newAttr = new XAttribute(member.Alias,
                                                attrValue is bool
                                                    ? Convert.ToString(attrValue, CultureInfo.InvariantCulture).ToLower()
                                                    : Convert.ToString((attrValue ?? String.Empty),
                                                                       CultureInfo.InvariantCulture));
            newLoc.Add(newAttr);
            return new XAttributeWrapper(newAttr, member.AttributeOrder);
        }

        /// <summary>
        ///   Finds the element with the given name located in the given location string in the given XML element.
        /// </summary>
        /// <param name = "baseElement">The XML element.</param>
        /// <param name = "location">The location string.</param>
        /// <param name = "elemName">Name of the element.</param>
        /// <returns>a value indicating whether the element with the given name located in 
        ///   the given location string in the given XML element has been found</returns>
        public static XElement FindElement(XElement baseElement, string location, string elemName)
        {
            return FindLocation(baseElement, StringUtils.CombineLocationAndElementName(location, elemName));
        }

        /// <summary>
        ///   Determines whether the XML element with the given name located in the 
        ///   given location string in the given XML element exists.
        /// </summary>
        /// <param name = "baseElement">The XML element.</param>
        /// <param name = "location">The location string.</param>
        /// <param name = "elemName">Name of the element.</param>
        /// <returns>a value indicating whether the XML element with the given name located in the 
        ///   given location string in the given XML element exists</returns>
        public static bool ElementExists(XElement baseElement, string location, string elemName)
        {
            return FindElement(baseElement, location, elemName) != null;
        }

        /// <summary>
        ///   Determines whether the XML element with the given name located in the 
        ///   given location string in the given XML element can be created
        /// </summary>
        /// <param name = "baseElement">The XML element.</param>
        /// <param name = "location">The location string.</param>
        /// <param name = "elemName">Name of the element.</param>
        /// <returns>
        ///   <c>true</c> if the XML element with the given name located in the given 
        ///   location string in the given XML element can be created; otherwise, <c>false</c>.
        /// </returns>
        public static bool CanCreateElement(XElement baseElement, string location, string elemName)
        {
            return CanCreateLocation(baseElement, StringUtils.CombineLocationAndElementName(location, elemName));
        }

        /// <summary>
        ///   Creates and returns the XML element with the given name located in the 
        ///   given location string in the given XML element.
        /// </summary>
        /// <param name = "baseElement">The parent XML element.</param>
        /// <param name = "location">The location string.</param>
        /// <param name = "elemName">Name of the element to create.</param>
        /// <returns>returns the XML element with the given name located in the 
        ///   given location string in the given XML element</returns>
        public static XElement CreateElement(XElement baseElement, string location, string elemName)
        {
            return CreateLocation(baseElement, StringUtils.CombineLocationAndElementName(location, elemName));
        }

        /// <summary>
        ///   Creates and returns the XML element with the given name located in the 
        ///   given location string in the given XML element.
        /// </summary>
        /// <param name = "baseElement">The parent XML element.</param>
        /// <param name = "location">The location string.</param>
        /// <param name = "elemName">Name of the element to create.</param>
        /// <param name = "elemValue">The element value to be assigned to the created element.</param>
        /// <returns>returns the XML element with the given name located in the 
        ///   given location string in the given XML element.</returns>
        public static XElement CreateElement(XElement baseElement, string location, string elemName, object elemValue)
        {
            XElement elem = CreateElement(baseElement, location, elemName);
            if(elem != null)
                elem.SetValue(elemValue);
            return elem;
        }

        /// <summary>
        ///   Moves all the children of src (including all its elements and attributes) to the 
        ///   destination element, dst.
        /// </summary>
        /// <param name = "src">The source element.</param>
        /// <param name = "dst">The destination element.</param>
        public static void MoveDescendants(XElement src, XElement dst)
        {
            foreach(XAttribute attr in src.Attributes())
            {
                if(dst.Attribute(attr.Name) != null)
                    throw new YmlAttributeAlreadyExistsException(attr.Name.ToString());

                dst.Add(attr);
            }

            foreach(XNode elem in src.Nodes())
                dst.Add(elem);
        }

        /// <summary>
        ///   Determines whether the specified element has neither any child attributes nor any child elements.
        /// </summary>
        /// <param name = "elem">The element.</param>
        /// <returns>
        ///   <c>true</c> if the specified element has neither any child attributes nor any child elements; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsElementCompletelyEmpty(XElement elem)
        {
            return !elem.HasAttributes && !elem.HasElements && elem.IsEmpty;
        }

        ///// <summary>
        ///// Returns a string representation for the content of the XML element including its child element if any
        ///// </summary>
        ///// <param name="elem">The elemenet</param>
        ///// <returns></returns>
        //public static string GetElementContent(this XElement elem)
        //{
        //    string value = elem.Value;

        //    if (String.IsNullOrEmpty(value) || String.IsNullOrEmpty(value.ToString()) || !elem.HasElements)
        //        return elem.Value;

        //    string content = elem.ToString();
        //    //string name = elem.Name.ToString();

        //    int closingBracketIndex = content.IndexOf('>');
        //    if (closingBracketIndex >= content.Length - 1)
        //        return value;

        //    int lastOpeneingBracket = content.LastIndexOf('<');
        //    if (lastOpeneingBracket <= 0)
        //        return value;

        //    string retValue = content.Substring(closingBracketIndex + 1, lastOpeneingBracket - closingBracketIndex - 1);
        //    return DecodeXMLString(retValue);
        //}

        /// <summary>
        ///   Decodes the XML escape sequences into normal string
        /// </summary>
        /// <param name = "str">The string to decode.</param>
        /// <returns></returns>
        public static string DecodeXmlString(string str)
        {
            if(str.IndexOf('&') >= 0)
            {
                return
                    str.Replace("&lt;", "<").Replace("&gt;", ">").Replace("&quot;", "\"").Replace("&apos;", "'").Replace
                        ("&amp;", "&");
                // Make sure that &amp; is the final replace so that sequences such as &amp;gt; do not get corrupted
            }

            return str;
        }

        /// <summary>
        ///   Get the absolute XPath to a given XElement 
        ///   (e.g. "/yml_catalog/shop/offers[0]/price").
        /// </summary>
        public static string GetAbsoluteXPath(this XElement element)
        {
            if(element == null)
                throw new ArgumentNullException("element");

            Func<XElement, string> relativeXPath = e =>
                                                   {
                                                       int index = e.IndexPosition();
                                                       string name = e.Name.LocalName;
                                                       // If the element is the root, no index is required 
                                                       return (index == -1)
                                                                  ? "/" + name
                                                                  : string.Format("/{0}[{1}]", name, index.ToString());
                                                   };

            var ancestors = from e in element.Ancestors()
                            select relativeXPath(e);

            return string.Concat(ancestors.Reverse().ToArray()) + relativeXPath(element);
        }

        /// <summary>
        ///   Get the index of the given XElement relative to its 
        ///   siblings with identical names. If the given element is 
        ///   the root, -1 is returned.
        /// </summary>
        /// <param name = "element"> 
        ///   The element to get the index of. 
        /// </param>
        public static int IndexPosition(this XElement element)
        {
            if(element == null)
                throw new ArgumentNullException("element");

            if(element.Parent == null)
                return -1;

            int i = 1; // Indexes for nodes start at 1, not 0 

            foreach(var sibling in element.Parent.Elements(element.Name))
            {
                if(sibling == element)
                    return i;
                i++;
            }

            throw new InvalidOperationException("element has been removed from its parent.");
        }
    }
}